﻿using jbt.CodeCandy.AsTrEAM.Contracts;
using System;
using System.Configuration;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.IO;
using System.Threading.Tasks;

namespace jbt.CodeCandy.AsTrEAM.StreamManager
{
    /// <summary>
    /// Implements an <see cref="IStreamManager"/> object what manages files in SQL Server.
    /// </summary>
    public class SqlServerFileStreamManager : IStreamManager
    {
        private async Task<NewStreamResult> CreateFile(StreamData fileData)
        {
            try
            {
                var newFile = new NewStreamResult();

                using (var connection = OpenConnection())
                {
                    using (var transaction = connection.BeginTransaction())
                    {
                        using (var command = connection.CreateCommand())
                        {
                            command.Transaction = transaction;

                            var id = await CreateFileCore(fileData, command);

                            newFile.Id = id;
                        }

                        transaction.Commit();
                    }
                }

                return newFile;
            }
            catch (StreamManagerException)
            {
                throw;
            }
            catch (Exception exception)
            {
                var message = String.Format("Failed to create file '{0}'. {1}", fileData.Name, exception.Message);

                throw new StreamManagerException(message, exception);
            }
        }

        /// <summary>
        /// Creates a new file in database and returns the unique identifier of new file.
        /// </summary>
        /// <param name="fileData">
        /// New file data to be added to database.
        /// </param>
        /// <param name="command">
        /// Command object initialized with connection and transaction.
        /// </param>
        /// <returns>
        /// The unique identifier of new file.
        /// </returns>
        protected virtual async Task<string> CreateFileCore(StreamData fileData, SqlCommand command)
        {
            var newFileId = Guid.NewGuid().ToString();

            command.CommandText = "insert into Files (Id, FileName, Content, ContentType, LastModified) output inserted.Content.PathName(), GET_FILESTREAM_TRANSACTION_CONTEXT() values (@Id, @FileName, 0x00, @ContentType, @LastModified)";

            command.Parameters.AddWithValue("@Id", newFileId);
            command.Parameters.AddWithValue("@FileName", fileData.Name);
            //command.Parameters.AddWithValue("@ContentEncoding", parameters.ContentEncoding);
            command.Parameters.AddWithValue("@ContentType", fileData.ContentType);
            command.Parameters.AddWithValue("@LastModified", fileData.LastModified);

            string path;
            byte[] transactionContext;

            using (var reader = command.ExecuteReader())
            {
                if (reader.Read())
                {
                    path = reader.GetString(0);
                    transactionContext = reader.GetSqlBytes(1).Buffer;
                }
                else
                {
                    const string message = "Something went wrong. Insert result couldn't be read.";
                    throw new InvalidOperationException(message);
                }
            }

            var sourceStream = fileData.Content;

            if (sourceStream != null)
            {
                using (var sqlFileStream = new SqlFileStream(path, transactionContext, FileAccess.Write))
                {
                    await sourceStream.CopyToAsync(sqlFileStream);
                }
            }

            return newFileId;
        }

        private async Task<StreamData> GetFile(string id)
        {
            // Connection should only be disposed when reading from stream completed.
            var connection = OpenConnection();

            try
            {
                var fileData = await GetFileCore(id, connection);

                return fileData;
            }
            catch (Exception exception)
            {
                connection.Close();

                if (exception is StreamManagerException)
                {
                    throw;
                }
                else
                {
                    var message = String.Format("Failed to read file '{0}'. {1}", id, exception.Message);

                    throw new StreamManagerException(message, exception);
                }
            }
        }

        private async Task<StreamData> GetFileCore(string id, SqlConnection connection)
        {
            var transaction = connection.BeginTransaction();

            try
            {
                using (var command = connection.CreateCommand())
                {
                    command.Transaction = transaction;

                    var fileData = await GetFileCore(id, command);

                    if (fileData == null)
                    {
                        throw new StreamManagerException("Null file data found.");
                    }

                    var auditStream = new AuditStream(id, fileData.Content, (auditData) =>
                    {
                        transaction.Commit();
                        connection.Close();
                    });

                    fileData.Content = auditStream;

                    return fileData;
                }
            }
            catch (Exception)
            {
                transaction.Rollback();

                throw;
            }
        }

        /// <summary>
        /// Reads a file from database.
        /// </summary>
        /// <param name="id">
        /// Unique identifier of file to read from database.
        /// </param>
        /// <param name="command">
        /// Command object initialized with connection and transaction.
        /// </param>
        /// <returns>
        /// The file with the specified unique identifier.
        /// </returns>
        /// <exception cref="StreamManagerException">
        /// File not found or can't be read for any reason.
        /// </exception>
        protected virtual async Task<StreamData> GetFileCore(string id, SqlCommand command)
        {
            command.CommandText = "select FileName, ContentEncoding, DATALENGTH(Content) as ContentLength, ContentType, LastModified, Content.PathName(), GET_FILESTREAM_TRANSACTION_CONTEXT() from Files where Id = @Id;";

            command.Parameters.AddWithValue("@Id", id);

            using (var reader = await command.ExecuteReaderAsync())
            {
                if (!reader.Read())
                {
                    var message = String.Format("File with identifier '{0}' not found.", id);
                    throw new StreamManagerException(message);
                }
                else
                {
                    var filename = reader.GetString(0);
                    var contentEncoding = Convert.ToString(reader.GetValue(1));
                    var contentLength = reader.GetInt64(2);
                    var contentType = reader.GetString(3);
                    var lastModified = reader.GetDateTime(4);
                    var path = reader.GetString(5);
                    var transactionContext = reader.GetSqlBytes(6).Buffer;

                    var sqlStream = new SqlFileStream(path, transactionContext, FileAccess.Read, FileOptions.Asynchronous | FileOptions.SequentialScan, allocationSize: 0);

                    var fileData = new StreamData
                    {
                        Content = sqlStream,
                        ContentEncoding = contentEncoding,
                        ContentType = contentType,
                        Name = filename,
                        Id = id,
                        LastModified = DateTime.Now,
                        ContentLength = contentLength,
                    };

                    return fileData;
                }
            }
        }

        /// <summary>
        /// Returns an open connection to the database where files are stored.
        /// </summary>
        /// <returns>
        /// An open connection to the database where files are stored.
        /// </returns>
        protected virtual SqlConnection OpenConnection()
        {
            const string connectionStringName = "jbt.CodeCandy.AsTrEAM.StreamManager.SqlServerStreamManager";

            var connectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];

            if (connectionStringSettings == null)
            {
                var message = String.Format("Connection string '{0}' not found in configuration.", connectionStringName);
                throw new InvalidOperationException(message);
            }

            var connectionString = connectionStringSettings.ConnectionString;

            var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
            connectionStringBuilder.AsynchronousProcessing = true;
            connectionStringBuilder.Pooling = true;

            var connection = new SqlConnection(connectionStringBuilder.ConnectionString);
            connection.Open();

            return connection;
        }

        #region IStreamManager Members

        async Task<NewStreamResult> IStreamManager.Create(StreamData fileData)
        {
            if (fileData == null)
            {
                throw new ArgumentNullException("fileData");
            }

            var newFile = await CreateFile(fileData);

            return newFile;
        }

        async Task<StreamData> IStreamManager.Get(string id)
        {
            if (String.IsNullOrWhiteSpace(id))
            {
                throw new ArgumentException("Id is null or white space.", "id");
            }

            var fileData = await GetFile(id);

            return fileData;
        }

        #endregion
    }
}